﻿using System;
using System.Collections.Generic;
using System.Reflection.Emit;
using Sigil.Impl;

namespace Sigil
{
	public partial class Emit<DelegateType>
	{
		/// <summary>
		/// Expects a reference to an array and an index on the stack.
		///
		/// Pops both, and pushes the element in the array at the index onto the stack.
		/// </summary>
		public Emit<DelegateType> LoadElement<ElementType>()
		{
			return LoadElement(typeof(ElementType));
		}

		/// <summary>
		/// Expects a reference to an array and an index on the stack.
		///
		/// Pops both, and pushes the element in the array at the index onto the stack.
		/// </summary>
		public Emit<DelegateType> LoadElement(Type elementType)
		{
			if (elementType == null)
			{
				throw new ArgumentNullException("elementType");
			}

			OpCode? instr = null;

			IEnumerable<StackTransition> transitions = null;

			var arrType = elementType.MakeArrayType();

			if (elementType.IsPointer)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType), arrType }, new [] { elementType }),
                        new StackTransition(new [] { typeof(Int32), arrType }, new [] { elementType }),
                    };

				instr = OpCodes.Ldelem_I;
			}

			if (!elementType.IsValueType && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType), arrType }, new [] { elementType }),
                        new StackTransition(new [] { typeof(Int32), arrType }, new [] { elementType }),
                    };

				instr = OpCodes.Ldelem_Ref;
			}

			if (elementType == typeof(sbyte) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType), arrType }, new [] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Int32), arrType }, new [] { typeof(Int32) }),
                    };

				instr = OpCodes.Ldelem_I1;
			}

			if (elementType == typeof(Byte) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType), arrType }, new [] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Int32), arrType }, new [] { typeof(Int32) }),
                    };

				instr = OpCodes.Ldelem_U1;
			}

			if (elementType == typeof(Int16) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType), arrType }, new [] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Int32), arrType }, new [] { typeof(Int32) }),
                    };

				instr = OpCodes.Ldelem_I2;
			}

			if (elementType == typeof(UInt16) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType), arrType }, new [] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Int32), arrType }, new [] { typeof(Int32) }),
                    };

				instr = OpCodes.Ldelem_U2;
			}

			if (elementType == typeof(Int32) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType), arrType }, new [] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Int32), arrType }, new [] { typeof(Int32) }),
                    };

				instr = OpCodes.Ldelem_I4;
			}

			if (elementType == typeof(UInt32) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType), arrType }, new [] { typeof(Int32) }),
                        new StackTransition(new [] { typeof(Int32), arrType }, new [] { typeof(Int32) }),
                    };

				instr = OpCodes.Ldelem_U4;
			}

			if ((elementType == typeof(Int64) || elementType == typeof(UInt64)) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType), arrType }, new [] { typeof(Int64) }),
                        new StackTransition(new [] { typeof(Int32), arrType }, new [] { typeof(Int64) }),
                    };

				instr = OpCodes.Ldelem_I8;
			}

			if (elementType == typeof(Single) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType), arrType }, new [] { typeof(Single) }),
                        new StackTransition(new [] { typeof(Int32), arrType }, new [] { typeof(Single) }),
                    };

				instr = OpCodes.Ldelem_R4;
			}

			if (elementType == typeof(Double) && !instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType), arrType }, new [] { typeof(Double) }),
                        new StackTransition(new [] { typeof(Int32), arrType }, new [] { typeof(Double) }),
                    };

				instr = OpCodes.Ldelem_R8;
			}

			if (!instr.HasValue)
			{
				transitions =
						new[]
                    {
                        new StackTransition(new [] { typeof(NativeIntType), arrType }, new [] { elementType }),
                        new StackTransition(new [] { typeof(Int32), arrType }, new [] { elementType }),
                    };

				UpdateState(OpCodes.Ldelem, elementType, Wrap(transitions, "LoadElement"));
				return this;
			}

			UpdateState(instr.Value, Wrap(transitions, "LoadElement"));

			return this;
		}
	}
}